package com.fzk.core;

import com.alibaba.fastjson2.JSON;
import com.fzk.core.btree.BTreeFile;
import com.fzk.core.btree.Page;
import com.fzk.env.conf.DataConf;
import com.fzk.env.conf.ServerConf;
import com.fzk.ha.HAMsg;
import com.fzk.ha.HAService;
import com.fzk.log.Logger;
import com.fzk.server.NettyHttpServer;
import com.fzk.utils.MyHmac;
import com.fzk.utils.MyFileUtil;
import com.fzk.vo.CheckAck;
import io.netty.buffer.ByteBuf;
import io.netty.channel.FileRegion;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Set;

/**
 * 文件储存服务
 * todo 优化：http文件分片上传时，每个分片都会open/close fileChannel, 过多的open和close
 * todo 组提交
 *
 * @author fzk
 * @datetime 2023-03-17 20:43
 */
public class FileStoreService implements AutoCloseable {
    private final NettyHttpServer server;
    public final FileMetaStore metaStore;
    public final FileIndex fileIndex;
    public final BTreeFile btree;
    public final String dataDir;
    private final ServerConf serverConf;

    public FileStoreService(NettyHttpServer server) throws IOException {
        this.server = server;
        this.serverConf = server.serverConf;
        DataConf dataConf = server.dataConf;
        this.dataDir = dataConf.getDataDir();
        // 先确保数据目录创建
        MyFileUtil.ensureDirs(Path.of(dataDir));
        // 元信息储存文件
        metaStore = new FileMetaStore(dataConf);
        // 索引文件
        fileIndex = new FileIndex(dataConf);
        // btree文件
        btree = new BTreeFile(Path.of(dataDir, dataConf.getBtreeFileName()), dataConf.getBtreeCachePageCount());
    }

    /**
     * 储存完整文件
     *
     * @param bucket       bucket存储桶
     * @param relativePath 桶内相对路径
     * @param filename     文件名
     * @param buf          文件内容
     * @return 返回文件访问key，默认为文件储存路径
     * @throws IOException e
     */
    public String fileStore(String bucket, String relativePath, String filename, ByteBuf buf) throws IOException {
        Path storagePath = generateStoragePath(bucket, relativePath, filename);
        return doStore(null, bucket, generateKey(storagePath, dataDir), storagePath, 0L, buf.readableBytes(), buf);
    }

    /**
     * 储存文件或分片
     * 第1次上传: 储存文件元信息得offset-->储存索引
     *
     * @param masterMeta 主节点发过来的元信息，只能用于主从同步情况，其它情况都应该设为null
     * @param path       文件存储路径
     * @param position   文件存储起点偏移量, 0L表示新文件弟第1次上传, 其它值表示分片上传
     * @param size       文件总大小
     * @param content    文件内容
     * @return 返回文件访问key，默认为文件储存路径
     * @throws IOException e
     */
    public String doStore(FileMeta masterMeta, String bucket, String key, Path path, long position, long size, ByteBuf content) throws IOException {
        // 1.存储文件或分片
        if (position == 0L && Files.exists(path)) {
            Logger.warning(String.format("文件 %s 已经存在, 并且position=0, 即将发生覆盖", path));
            Files.deleteIfExists(path);// 删除已存在文件
        }
        MyFileUtil.ensureDirsAndFile(path);// 确保创建文件
        long writePos = position;
        try (FileChannel fileChannel = FileChannel.open(path, Set.of(StandardOpenOption.WRITE))) {
            while (content.isReadable()) {
                writePos += content.readBytes(fileChannel, writePos, content.readableBytes());
            }
            fileChannel.force(true);// 落盘
        }

        // 文件第一次上传，需保存元信息
        if (position == 0L) {
            // 2.存储元信息得到offset
            // 注意：主节点发过来的元信息，只能用于主从同步情况，其它情况都应该设为null
            // 3.构建hash索引
            // 4.构建btree索引
            putMeta(masterMeta, path, key, bucket, writePos == size ? FileMeta.UploadSuccess : FileMeta.Uploading, size);
        }
        // 5.分片上传，且最后一片上传完成
        // 校验：元信息size和文件大小是否一致
        // 更新：元信息md5和uploadStatus
        if (position != 0L && writePos == size) {
            // 5.1 获取meta信息
            FileMetaStore.FileMetaWrapper wrapper = getFileMeta(key);
            if (wrapper == null) {
                Logger.error(String.format("文件元信息校验失败: 分片上传最后一分片上传成功，此时索引文件中不存在key: %s，无法获取到元信息?", key));
                return key;
            }
            sliceUploadFinish(path, wrapper, writePos);
        }
        return key;
    }

    public FileMetaStore.FileMetaWrapper putMeta(FileMeta masterMeta, Path storagePath, String key, String bucket, byte uploadStatus, long size) throws IOException {
        // 2.存储元信息得到offset
        // 注意：主节点发过来的元信息，只能用于主从同步情况，其它情况都应该设为null
        FileMeta meta;
        if (masterMeta != null) {
            meta = masterMeta;
            if (!HAService.Replica.equals(serverConf.getRole())) {
                Logger.error(String.format("只有从节点用于主从同步时, 此方法参数masterMeta才能有值，其它情况都必须为null, masterMeta: %s", metaStore));
                throw new RuntimeException("只有从节点用于主从同步时, 此方法参数masterMeta才能有值，其它情况都必须为null");
            }
            // md5和上传状态在从节点是独立计算的
            meta.setMd5(MyHmac.getFileMD5(storagePath));
            meta.setUploadStatus(uploadStatus);
        } else {
            meta = new FileMeta();
            meta.setKey(key);
            meta.setUploadStartTimeStamp(System.currentTimeMillis());
            meta.setUploadEndTimeStamp(System.currentTimeMillis());
            meta.setMd5(MyHmac.getFileMD5(storagePath));
            meta.setFilename(storagePath.getFileName().toString());
            meta.setSize(size);
            meta.setBucket(bucket);
            meta.setRelativePath(relativePath(storagePath, dataDir));// 去掉数据存储目录
            meta.setUploadStatus(uploadStatus);
            meta.setDeleted((byte) 0);
        }

        FileMetaStore.FileMetaWrapper metaWrapper = metaStore.writeMeta(meta);
        // 3.构建hash索引
        // 4.构建btree索引
        putIndex(metaWrapper);
        return metaWrapper;
    }

    public void putIndex(FileMetaStore.FileMetaWrapper metaWrapper) throws IOException {
        FileMeta meta = metaWrapper.meta;
        long offset = metaWrapper.offset;

        // 3.构建hash索引
        fileIndex.putIndex(meta, offset, this.metaStore);

        // 4.构建btree索引
        btree.put(meta.getKey(), offset);
    }

    /**
     * 文件分片上传结束, 校验文件
     *
     * @param path      文件储存路径
     * @param wrapper   元信息
     * @param finalSize 文件最后大小
     * @throws IOException 可能的i/o异常
     */
    public void sliceUploadFinish(Path path, FileMetaStore.FileMetaWrapper wrapper, long finalSize) throws IOException {
        long offset = wrapper.offset;
        FileMeta meta = wrapper.meta;
        // 5.2 校验文件大小
        if (meta.getSize() != finalSize) {
            String str = String.format("文件元信息校验失败: 元信息存储的size: %d 和 文件最终大小: %d不一致. 元信息存储的size是第一次上传文件分片时记录前端传递的文件总大小", meta.getSize(), finalSize);
            Logger.error(str);
            throw new RuntimeException(str);
        }
        // 5.3 校验md5长度
        if (meta.getMd5() == null || meta.getMd5().length() != MyHmac.MD5_Str_Len) {
            Logger.error(String.format("文件元信息校验失败: 分片上传最后一分片上传成功，但是之前存入的元信息中md5长度不为%d, meta: %s", MyHmac.MD5_Str_Len, meta));
            return;
        }
        // 5.4 更新完整的md5、uploadStatus、上传结束时间戳
        String oldMd5 = meta.getMd5();
        meta.setMd5(MyHmac.getFileMD5(path));
        meta.setUploadEndTimeStamp(System.currentTimeMillis());
        meta.setUploadStatus(FileMeta.UploadSuccess);
        byte[] metaBytes = JSON.toJSONString(meta).getBytes(StandardCharsets.UTF_8);
        if (wrapper.metaLen != metaBytes.length) {
            String str = String.format("文件元信息更新失败: 分片上传最后一分片上传成功，准备更新元信息，但是修改元信息序列化为字节数组后其长度: %d和原本的metaLen: %d不一致，必须保证一致，否则造成严重影响, meta: %s", metaBytes.length, wrapper.metaLen, meta);
            Logger.fatal(str);
            throw new RuntimeException(str);
        }
        metaStore.updateMeta(offset, meta, metaBytes);
        Logger.info(String.format("文件元信息更新成功: 分片上传: 最后一分片上传完成，文件分片上传成功，且已经重新计算并更新了md5，旧md5: %s, 新meta: %s", oldMd5, meta));

        // 6.主节点通知从节点进行特殊同步：单个文件同步
        if (HAService.Master.equals(serverConf.getRole()) && serverConf.isOpenHaService()) {
            // 是主节点并且开启了主从同步功能
            this.server.haService.channels.forEach(((channelId, channel) -> {
                HAMsg.HASyncResponseMsg resMsg = new HAMsg.HASyncResponseMsg(new HAMsg.HASyncResponseHeader(HAMsg.HASyncRequestHeader.SingleFileSync, offset, -1L, wrapper.metaLen, meta), (FileRegion) null);
                channel.writeAndFlush(resMsg.serialize());
            }));
        }
    }

    public int prefixSearchCount(String prefix) {
        return btree.prefixSearchCount(prefix);
    }

    public ArrayList<Page.BEntry> prefixSearch(String prefix, int offset, int limit) {
        return btree.prefixSearch(prefix, offset, limit);
    }

    public FileMetaStore.FileMetaWrapper deleteFile(String key) throws IOException {
        // 1.立刻删除hash索引
        FileIndex.IndexWrapper indexWrapper = fileIndex.deleteIndex(key, metaStore);
        // 2.再删除btree索引
        Long oldOffset = btree.remove(key);
        if (indexWrapper == null && oldOffset == null) {
            Logger.warning(String.format("准备删除key: %s，但是未在索引中找到其数据", key));
            return null;// 没有索引，无法找到文件存储路径
        } else if (indexWrapper == null || oldOffset == null) {// 两条索引只存在其中一条
            Logger.warning(String.format("key: %s对应的索引数据不全, hash索引: %s, btree索引: key-->%s", key, indexWrapper, oldOffset));
        } else {
            // 两条索引数据都存在，必须保证其offset一致
            if (indexWrapper.index.offset != oldOffset) {
                Logger.error(String.format("哈希索引和Btree索引指向的offset不一致, key: %s, hash: %s, btree: key-->%d", key, indexWrapper, oldOffset));
            }
        }
        // 3.文件元信息标记删除
        long offset = indexWrapper != null ? indexWrapper.index.offset : oldOffset;// 优先用哈希索引的数据
        FileMetaStore.FileMetaWrapper metaWrapper = metaStore.readMeta(offset);
        if (metaWrapper.meta.getDeleted() == (byte) 0) {
            metaWrapper.meta.setDeleted((byte) 1);// 标记删除
            metaStore.updateMeta(offset, metaWrapper.meta, JSON.toJSONBytes(metaWrapper.meta));
            // 4.删除文件
            String relativePath = metaWrapper.meta.getRelativePath();
            Path filePath = Path.of(dataDir, relativePath);
            Files.deleteIfExists(filePath);// 删除文件
            Logger.info(String.format("删除文件key: %s, meta: %s", key, metaWrapper));
            // 5.判断是否需要删除空目录
            Path parentDir = filePath.getParent();
            while (!parentDir.equals(Path.of(dataDir)) && Files.isDirectory(parentDir)) {
                String[] children = parentDir.toFile().list();
                if (children == null || children.length == 0) {
                    Logger.info("删除空目录: " + parentDir);
                    Files.deleteIfExists(parentDir);
                }
                parentDir = parentDir.getParent();
            }
        }
        return metaWrapper;
    }

    /**
     * 暂时用相对存储路径的md5作为key
     */
    public static String generateKey(Path storagePath, String dataDir) {
        return MyHmac.md5(relativePath(storagePath, dataDir).getBytes(StandardCharsets.UTF_8));
    }

    public static String relativePath(Path storagePath, String dataDir) {
        Path rootDataDir = Path.of(dataDir);
        Path relativePath = rootDataDir.relativize(storagePath);// 去掉数据存储目录，方便直接ha service，方便直接修改数据目录
        return MyFileUtil.toUnixPath(relativePath.toString());
    }

    public Path generateStoragePath(String relativePath) {
        return Path.of(this.dataDir, relativePath);
    }

    public Path generateStoragePath(String bucket, String relativePath, String filename) {
        return Path.of(this.dataDir, bucket, relativePath, filename);
    }

    /**
     * 文件分片存储
     *
     * @param bucket       存储桶
     * @param relativePath 桶内相对路径
     * @param filename     文件名
     * @param position     分片偏移量, 0L表示第1次上穿，将存储元信息，其它值不存储元信息
     * @param size         文件总大小
     * @param buf          文件分片内容
     * @return 文件访问key
     * @throws IOException e
     */
    public String fileSliceStore(String bucket, String relativePath, String filename, long position, long size, ByteBuf buf) throws IOException {
        // 分片上传
        Path storagePath = generateStoragePath(bucket, relativePath, filename);
        Logger.debug(String.format("文件分片存储, bucket: %s, path: %s", bucket, storagePath));
        return doStore(null, bucket, generateKey(storagePath, this.dataDir), storagePath, position, size, buf);
    }

    /**
     * 文件断点检查, 简单直接到检查文件的长度即可
     */
    public CheckAck fileSliceCheck(String bucket, String relativePath, String filename) throws IOException {
        Path storagePath = generateStoragePath(bucket, relativePath, filename);
        String key = generateKey(storagePath, dataDir);
        CheckAck ack;
        FileMetaStore.FileMetaWrapper metaWrapper = getFileMeta(key);
        if (metaWrapper != null) {
            // 1.元信息存在
            FileMeta meta = metaWrapper.meta;
            long position = 0;
            if (meta.getUploadStatus() == FileMeta.UploadSuccess) {
                position = meta.getSize();
            } else if (Files.exists(storagePath)) {
                position = Files.size(storagePath);
            }
            ack = new CheckAck(position, key, meta.getUploadStatus(), true, true, meta.getMd5());
        } else {
            // 2.不存在元信息
            if (Files.exists(storagePath)) {
                // 3.存在文件则计算md5，用于前端判断是否为同一个文件
                ack = new CheckAck(Files.size(storagePath), key, FileMeta.UnUpload, false, true, MyHmac.getFileMD5(storagePath));
            } else {
                ack = new CheckAck(0, key, FileMeta.UnUpload, false, false, null);
            }
        }
        Logger.debug(String.format("文件断点检查, bucket: %s, path: %s, key: %s, checkAck: %s", bucket, storagePath, key, ack));
        return ack;
    }

    /**
     * 根据key获取文件访问path
     *
     * @param key 文件唯一key
     * @return 不存在返回null
     */
    public Path getStorePath(String key) {
        FileMetaStore.FileMetaWrapper fileMeta = getFileMeta(key);
        if (fileMeta != null) {
            return generateStoragePath(fileMeta.meta.getRelativePath());
        }
        return null;
    }

    /**
     * 根据key获取文件存储元信息
     *
     * @param key 文件唯一key
     * @return 不存在返回null
     */
    public FileMetaStore.FileMetaWrapper getFileMeta(String key) {
        if (key == null || key.length() == 0) {
            return null;
        }
        FileIndex.IndexWrapper indexWrapper = fileIndex.getIndex(key, metaStore);
        if (indexWrapper != null) {
            return metaStore.readMeta(indexWrapper.index.offset);
        }
        return null;
    }

    @Override
    public void close() throws IOException {
        metaStore.close();
        fileIndex.close();
    }
}
